-- scatter: scatter a single specification into database tables,
--          database types, database functions, a Java backend,
--          an HTML frontend, a JavaScript AJAX API,
--          and a beautiful database document written in HTML.


local map = function(a, f)
    if a == nil then return nil end
    local result = {}
    for i,v in ipairs(a) do
        result[i] = f(v, i)
    end
    return result
end

local each = function(a, f)
    if a == nil then return end
    for i,v in ipairs(a) do
        f(v, i)
    end
end

local map_over = function(tbl, keys, f)
    if tbl == nil then return nil end
    local result = {}
    local i = 1
    for k in keys:gmatch("%S+") do
        result[i] = f(tbl[k], k)
        i = i + 1
    end
    return result
end

local make_2d_table = function(keys)
    local tbl = {}
    for k in keys:gmatch("%S+") do
        tbl[k] = {}
    end
    return tbl
end


local lines = function(tbl)
    return table.concat(tbl, "\n")
end

local clines = function(sep, tbl)   -- commented lines
    local len = #tbl
    return table.concat(map(tbl, function(line, i)
        return ("%s%s%s"):format(line[1], (i==len and "" or sep), line[2] or "")
    end), "\n")
end


local function make_sql(spec)
    local  in_tags = [[enum entity]]
    local out_tags = [[enum table function]]
    local meta = make_2d_table(in_tags)
    local generated = make_2d_table(out_tags)

    local gen = function(tag, text)
        table.insert(generated[tag], text)
    end

    local dispatcher = {}
    local make_dispatcher = function(name)
        return function(spec)
            if spec.tag == nil then error("spec should contain a tag") end
            local s = dispatcher[name][spec.tag]
            if s == nil then error(("no such %s: %s"):format(name, spec.tag)) end
            return s(spec)
        end
    end

    local stmt = make_dispatcher('stmt')
    local constraint = make_dispatcher('constraint')
    local fn_body = make_dispatcher('fn_body')

    local header = ("-"):rep(76)
    local fold = function(desc, x)
        local header = ([[-- <editor-fold desc="%s">]]):format(desc)
        local footer = [[-- </editor-fold>]]
        return lines { header, x, footer }
    end

    dispatcher.stmt = {
        enum = function(spec)
            gen('enum', lines {
                fold(spec.name, lines {
                    header,
                    ("-- Type: %s"):format(spec.name),
                    ("CREATE TYPE %s AS ENUM (%s"):format(spec.name, spec._ and "    -- " .. spec._ or ""),
                    clines(",", map(spec, function(enumerator)
                        local comment = enumerator._
                        if comment ~= nil then comment = "    -- " .. comment end
                        return { ("  '%s'"):format(enumerator[1]), comment }
                    end)),
                    ");",
                }),
                "",
            })
        end,

        entity = function(spec)
            local mdata = meta.entity.data[spec.name]
            local table_name = spec.name .. "s"
            meta.entity.active_data = mdata
            meta.entity.active_table_name = table_name
            mdata.field_type = {}

            -- construct table
            local fields = map(spec.field, function(f)
                local name, type, comment = f[1], f[2], f._
                mdata.field_type[name] = type
                if comment ~= nil then comment = "    -- " .. comment end
                return { ("  %s %s"):format(name, type), comment }
            end)
            each(spec.constraint, function(c)
                local comment = c._
                if comment ~= nil then comment = "    -- " .. comment end
                table.insert(fields, { "  " .. constraint(c), comment })
            end)

            gen("table", lines {
                fold(table_name, lines {
                    header,
                    ("-- Table: %s%s"):format(table_name, spec._ and "   -- " .. spec._ or ""),
                    ("CREATE TABLE %s"):format(table_name),
                    "(",
                    clines(",", fields),
                    ");",
                }),
                "",
            })

            -- construct functions
            each(spec.fn, function(fns)
                local fn_name = spec.name .. "_" .. fns.name
                local raw_args = map(fns.args, function(a) return a:gsub("^%L", "") end)
                local params = map(raw_args, function(a) return a .. "_ text" end)
                params = table.concat(params, ", ")
                local ret = "void"
                gen("function", lines {
                    fold(fn_name, lines {
                        header,
                        ("-- Function: %s%s"):format(fn_name, fns._ and "    -- " .. fns._ or ""),
                        ([[CREATE FUNCTION %s(%s)]]):format(fn_name, params),
                        ([[  RETURNS %s AS]]):format(ret),
                        [[$BODY$]],
                        [[BEGIN]],
                        lines(map(fns, function(body) return fn_body(body) end)),
                        [[END;]],
                        [[$BODY$]],
                        [[  LANGUAGE plpgsql]],
                        [[  SECURITY DEFINER;]],
                    }),
                    "",
                })
            end)
        end,
    }

    dispatcher.constraint = {
        pkey = function(spec)
            return ([[CONSTRAINT %s_pkey PRIMARY KEY (%s)]]):format(meta.entity.active_table_name, spec[1])
        end,
        unique = function(spec)
            return ([[CONSTRAINT %s_%s_unique UNIQUE (%s)]]):format(
                meta.entity.active_table_name,
                table.concat(spec, "_"),
                table.concat(spec, ","))
        end,
    }

    dispatcher.fn_body = {
        insert = function(spec)
            local ws = spec.what
            local targets = map(ws, function(w) return w[1] end)
            targets = table.concat(targets, ", ")

            local values = map(ws, function(w)
                local target = w[1]
                local value = w[2] or target
                if value:sub(1,1) == '=' then
                    value = value:sub(2)
                else
                    local target_type = meta.entity.active_data.field_type[target]
                    if target_type == nil then error("no such field: " .. target) end
                    value = value .. "_"
                    if target_type ~= 'text' then value = value .. "::" .. target_type end
                end
                return value
            end)
            values = table.concat(values, ", ")

            return lines {
                ([[  INSERT INTO %s(%s)]]):format(meta.entity.active_table_name, targets),
                ([[      VALUES (%s);]]):format(values),
            }
        end,

        call = function(spec)
            local target = meta.entity.active_data.spec.name .. "_" .. spec.name
            local values = map(spec.args, function(a)
                if a:sub(1,1) == '=' then
                    return a:sub(2)
                end
                return a .. "_"
            end)
            values = table.concat(values, ", ")

            return lines {
                ([[  PERFORM %s(%s);]]):format(target, values),
            }
        end
    }

    -- populate meta data
    for _,s in ipairs(spec) do
        local m = meta[s.tag]
        if m == nil then error("unknown tag: " .. s.tag) end
        if s.name == nil or s.name == "" then error("name required: " .. s.tag) end
        if m.data == nil then m.data = {} end
        if m.data[s.name] ~= nil then error("duplicate name: " .. s.name) end
        m.data[s.name] = { spec = s }
    end

    -- compile all statements
    for _,s in ipairs(spec) do
        stmt(s)
    end

    -- join the result
    return lines {
        "",
        lines(map_over(generated, out_tags, function(g, tag)
            return lines { fold(tag .. "s", lines(g)), "" }
        end)),
        "",
    }
end

local function make_java(spec)
end

local sugar = require[[scatter-sugar]]
local populate_defaults = function(spec)
    each(spec, function(s)
        if s.tag == 'entity' then
            if s.field == nil then error("entity must have field: " .. s.name) end
            table.insert(s.field, 1, { "id", "serial", _="自动递增序列" })
            if s.constraint == nil then s.constraint = {} end
            table.insert(s.constraint, 1, { tag = "pkey", "id", _="主键" })
        end
    end)
    return spec
end

return function(src)
    local spec, err = load(src, "spec", "bt", sugar)
    if spec == nil then error(err) end
    spec = populate_defaults(spec())
    return {
        sql = make_sql(spec),
        java = make_java(spec),
    }
end

